﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Dynamic;
using System.Linq;
using System.Net;
using System.Text;
using System.Xml;

namespace UPNP.IO
{
    partial class DeviceService
    {

#if false
        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
        {
            InvokeAction(binder.Name, args, );
        }

        /// <summary>
        /// invokes an action on the service
        /// </summary>
        protected void InvokeAction(string name, object[] inArgs, out object[] outArgs)
        {
            if (_logIO.IsDebugEnabled)
            {
                _logIO.DebugFormat(
                    "Invoking action {0} on service '{1}'",
                    GetInvokeActionDebugString(name, inArgs, null),
                    this.ServiceTypeIdentifier
                    );
            }

            // cached?
            if (_notImplementedCache.Contains("A_" + name))
            {
                throw new NotImplementedException(string.Format(
                    "action {0} not implemented on service {1} (cached)",
                    name,
                    this.ServiceTypeIdentifier
                    ));
            }

            // invoke then
            try
            {
                InvokeActionInternal(name, inArgs, out outArgs);
            }
            catch (NotImplementedException)
            {
                _notImplementedCache.Add("A_" + name);

                throw;
            }
        }

        private void InvokeActionInternal(string name, object[] inArgs, out object[] outArgs)
        {
            try
            {
                // connection lost?
                if (this.ConnectionLost)
                    throw new DeviceConnectionLostException(_parentDevice);

                // continue
                // invoke
                object outArgsTemp = new object[0];

                var ret = _service.InvokeAction(name, (object)inArgs, ref outArgsTemp);

                outArgs = (object[])outArgsTemp;

                // done
                if (_logIO.IsDebugEnabled)
                {
                    _logIO.DebugFormat(
                        "Invoked action {1} returned: \"{0}\"",
                        ret,
                        GetInvokeActionDebugString(name, inArgs, outArgs)
                        );
                }

            }
            catch (COMException e)
            {
                var err = (uint)e.ErrorCode;
                var errStr = e.ToErrorCodeString();

                // method not implemented by upnp device?
                if (err == (uint)COMExceptionErrorCode.UPNP_E_INVALID_ACTION)
                {
                    if (_logIO.IsDebugEnabled)
                    {
                        _logIO.Debug(string.Format(
                            "Failed to invoke action {0} on service '{1}': not implemented ({2})",
                            GetInvokeActionDebugString(name, inArgs, null),
                            this.ServiceTypeIdentifier,
                            errStr
                            ), e);
                    }

                    // finalize
                    throw new NotImplementedException(string.Format(
                        "action {0} not implemented on service {1} ({2})",
                        name,
                        this.ServiceTypeIdentifier,
                        errStr
                        ), e);
                }

                // connection lost?
                if ((err == (uint)COMExceptionErrorCode.UPNP_E_DEVICE_TIMEOUT) ||
                    (err == (uint)COMExceptionErrorCode.UPNP_E_TRANSPORT_ERROR))
                {
                    if (!_parentDevice.RootDevice.ConnectionLost)
                        _parentDevice.RootDevice.SetConnectionLost();

                    throw new DeviceConnectionLostException(_parentDevice);
                }

                // any other error is handled normally (see below)
                _logIO.Error(string.Format(
                    "Failed to invoke action {0} on service '{1}' ({2})",
                    GetInvokeActionDebugString(name, inArgs, null),
                    this.ServiceTypeIdentifier,
                    errStr
                    ), e);

                throw;
            }
            catch (NotImplementedException)
            {
                // just passing through
                throw;
            }
            catch (DeviceConnectionLostException)
            {
                if (!_parentDevice.RootDevice.ConnectionLost)
                    _parentDevice.RootDevice.SetConnectionLost();

                throw;
            }
            catch (Exception e)
            {
                _logIO.Error(string.Format(
                    "Failed to invoke action {0} on service '{1}'",
                    GetInvokeActionDebugString(name, inArgs, null),
                    this.ServiceTypeIdentifier
                    ), e);

                throw;
            }
        }
#endif

        #region soap request

        internal class InvokeActionQuery : DeviceServiceQuery
        {
            protected readonly DeviceDom.Action _dom;

            protected override void PrepareRequest(HttpWebRequest req)
            {
                req.Method = "POST";

                req.Headers.Add("SOAPACTION", (_dom.ParentService.ServiceTypeId + "#" + _dom.ActionName));
            }

            protected override void WriteRequest(XmlTextWriter xw)
            {
                xw.WriteStartDocument();
                {
                    _dom.ToSoapRequest(xw, this.Arguments);
                }
                xw.WriteEndDocument();
            }

            protected override object HandleResponse(HttpWebResponse response, XmlDocument body)
            {
                // find output arguments
                var resElement = (XmlElement)body.SelectSingleNode(
                    "/*[local-name()='Envelope']/*[local-name()='Body']/*[local-name()='" + _dom.ActionName + "Response']"
                    );

                if (resElement == null)
                {
                    throw new DeviceCommunicationException("action response element not found");
                }

                // read output arguments
                var r = new DynamicReturn();

                foreach (var a in _dom.GetOutArguments())
                {
                    var val = resElement.SelectSingleNodeInnerTextByLocalName(a.ArgumentName, false);
                    var valParsed = a.RelatedVariable.ValueConverter.FromDevice(val);

                    r.Add(a.ArgumentName, valParsed);
                }

                // convert the value
                return r;
            }

            public InvokeActionQuery(DeviceDom.Action v)
                : base(v.ParentService, _logIO)
            {
                Contract.Requires(v != null);

                _dom = v;
            }
        }

        internal sealed class InvokeActionQueryMan : InvokeActionQuery
        {
            protected override void PrepareRequest(HttpWebRequest req)
            {
                req.Method = "M-POST";
                req.ContentType = "text/xml; charset=\"" + SoapEncoding.WebName + "\"";

                req.Headers.Add("MAN", "\"http://schemas.xmlsoap.org/soap/envelope/\"; ns=01");
                req.Headers.Add("01-SOAPACTION", (_dom.ParentService.ServiceTypeId + "#" + _dom.ActionName));
            }

            protected override object HandleResponse(HttpWebResponse response, XmlDocument body)
            {
                // ensure MAN is supported
                if (!response.Headers.AllKeys.Contains("EXT"))
                    throw new DeviceCommunicationException("MAN/EXT not understood");

                // continue
                return base.HandleResponse(response, body);
            }

            public InvokeActionQueryMan(DeviceDom.Action v)
                : base(v)
            {
                Contract.Requires(v != null);
            }
        }

        #endregion
    }
}
